package org.covalence.core.visitor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.covalence.core.Constants;
import org.covalence.core.PropertyConfig;
import org.covalence.core.Registry;
import org.covalence.core.Strings;
import org.covalence.core.util.CTable;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * ASM Visitor class which writes new fields and methods to add PropertyChangeSupport
 * to a visited POJO class.  The enhanced classes can be nested inside one another
 * and be notified if it changes in its parent class.  For example,  if a parent
 * class contains a child class and the child class has properties which are 
 * listened to by some widgets and the setChild method is called on the parent 
 * class, then all of the widgets watching child properties will be notified 
 * as well.  It can also be notified whenever collection objects are changed 
 * via the collection objects methods.
 * 
 * In addition it will automatically nest any child objects and hook up the 
 * necessary listeners whenever a setMethod is called and the parameter is
 * an Observable object.  It will also auto nest Collection objects by providing
 * a wrapper Collection whenever a regular collection is used.
 * 
 * @author tslusser
 */
public class BoundClassVisitor extends ClassAdapter 
		implements Constants, Opcodes {
	
	private String className;

	private static Logger log = Logger.getLogger(BoundClassVisitor.class);
	
	public BoundClassVisitor(ClassVisitor cv) {
		super(cv);
	}

	@Override
	public void visit(int version, int access, String name, String signature,
			String superName, String[] interfaces) {
		
		this.className = name;
		
		List<String> c = interfaces == null ? new ArrayList<String>() : 
			new ArrayList<String>(Arrays.asList(interfaces));

		c.add(OBSERVABLE);

		// - add the PropertyChangeSupport field
		cv.visitField(ACC_PRIVATE, PROPERTY_CHANGE_SUPPORT_FIELD, 
				CPROPERTY_CHANGE_SUPPORT_TYPE, null, null);
				
		// - add the parent field
		cv.visitField(ACC_PRIVATE, PARENT_FIELD,
				OBSERVABLE_TYPE, null, null);
		
		// - add the nested property field
		cv.visitField(ACC_PRIVATE, NESTED_PROPERTY_FIELD, 
				"Ljava/lang/String;", null, null);
		
		super.visit(version, access, name, signature, superName, 
				(String[]) c.toArray(new String[c.size()]));
	}

	@Override
	public FieldVisitor visitField(int access, String name, String desc, 
			String signature, Object value) {
		
		log.trace("[classname:" + this.className +  
				", access:" + Strings.access(access) + 
				", name:" + name +
				", desc:" + desc + 
				", signature:" + signature +
				", value:" + value + "]");
		return super.visitField(access, name, desc, signature, value);
	}

	@Override
	public void visitEnd() {
		
		createMethod_addPropertyChangeListener();
		createMethod_removePropertyChangeListener();
		createMethod_addNestedObservable();
		createMethod_setNestedPropertyName();
		createMethod_getParent();
		createMethod_setParent();
		createMethod_propertyChange();
		createMethod_getPropertyChangeListeners();
		createMethod_fireAll();
		
		super.visitEnd();
	}

	private void createMethod_addNestedObservable() {

		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PUBLIC, "addNestedObservable", 
				ADD_NESTED_OBSERVABLE_SIG, 
				null, null);
		mv.visitCode();
		Label l0 = new Label();
		mv.visitLabel(l0);
		mv.visitLineNumber(67, l0);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitVarInsn(ALOAD, 2);
		mv.visitMethodInsn(INVOKEINTERFACE, OBSERVABLE, 
				"setNestedPropertyName", "(Ljava/lang/String;)V");
		Label l1 = new Label();
		mv.visitLabel(l1);
		mv.visitLineNumber(68, l1);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", 
				"()Ljava/lang/Class;");
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", 
				"()Ljava/lang/String;");
		mv.visitMethodInsn(INVOKESTATIC, CTABLE, 
				"isCollectionType", "(Ljava/lang/String;)Z");
		Label l2 = new Label();
		mv.visitJumpInsn(IFNE, l2);
		Label l3 = new Label();
		mv.visitLabel(l3);
		mv.visitLineNumber(69, l3);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, this.className, 
				"addPropertyChangeListener", 
				"(Ljava/beans/PropertyChangeListener;)V");
		mv.visitLabel(l2);
		mv.visitLineNumber(71, l2);
		mv.visitInsn(RETURN);
		Label l4 = new Label();
		mv.visitLabel(l4);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}
	
	private void createMethod_setNestedPropertyName() {
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PUBLIC, "setNestedPropertyName", 
				"(Ljava/lang/String;)V", null, null);
		mv.visitCode();
		Label l_0 = new Label();
		mv.visitLabel(l_0);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitFieldInsn(PUTFIELD, this.className, NESTED_PROPERTY_FIELD,
				"Ljava/lang/String;");
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}
	
	private void createMethod_removePropertyChangeListener() {
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PUBLIC, "removePropertyChangeListener",
				"(Ljava/beans/PropertyChangeListener;)V", null, null);
		mv.visitCode();
		Label l_0 = new Label();
		mv.visitLabel(l_0);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, className, PROPERTY_CHANGE_SUPPORT_FIELD,
				CPROPERTY_CHANGE_SUPPORT_TYPE);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, CPROPERTY_CHANGE_SUPPORT,
				"removePropertyChangeListener",
				"(Ljava/beans/PropertyChangeListener;)V");
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	private void createMethod_addPropertyChangeListener() {
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PUBLIC, "addPropertyChangeListener",
				"(Ljava/beans/PropertyChangeListener;)V", null, null);
		mv.visitCode();
		Label l_0 = new Label();
		mv.visitLabel(l_0);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, className, PROPERTY_CHANGE_SUPPORT_FIELD,
				CPROPERTY_CHANGE_SUPPORT_TYPE);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, CPROPERTY_CHANGE_SUPPORT,
				"addPropertyChangeListener",
				"(Ljava/beans/PropertyChangeListener;)V");
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}
	
	private void createMethod_propertyChange() {
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PUBLIC, "propertyChange", 
				"(Ljava/beans/PropertyChangeEvent;)V", null, null);
		mv.visitCode();
		// if(evt.getSource() == _parent && 
		//    evt.getPropertyName().equals(_nestedPropertyName))
		// {
		Label l0 = new Label();
		mv.visitLabel(l0);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/beans/PropertyChangeEvent", 
				"getSource", "()Ljava/lang/Object;");
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, this.className, PARENT_FIELD, 
				OBSERVABLE_TYPE);
		Label l1 = new Label();
		mv.visitJumpInsn(IF_ACMPNE, l1);
		Label l2 = new Label();
		mv.visitLabel(l2);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/beans/PropertyChangeEvent", 
				"getPropertyName", "()Ljava/lang/String;");
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, this.className, NESTED_PROPERTY_FIELD, 
				"Ljava/lang/String;");
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", 
				"(Ljava/lang/Object;)Z");
		mv.visitJumpInsn(IFEQ, l1);

		// fireAll();
		Label l3 = new Label();
		mv.visitLabel(l3);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKESPECIAL, this.className, "fireAll", "()V");
		Label l4 = new Label();
		mv.visitJumpInsn(GOTO, l4);
		mv.visitLabel(l1);
		// }
		// else
		// {
		// _pcs.firePropertyChange(event)
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, this.className, 
				PROPERTY_CHANGE_SUPPORT_FIELD, 
				CPROPERTY_CHANGE_SUPPORT_TYPE);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitMethodInsn(INVOKEVIRTUAL, CPROPERTY_CHANGE_SUPPORT, 
				"firePropertyChange", "(Ljava/beans/PropertyChangeEvent;)V");
		mv.visitLabel(l4);
		// }
		mv.visitInsn(RETURN);
		Label l5 = new Label();
		mv.visitLabel(l5);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	private void createMethod_getParent() {
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PUBLIC, "getParent", 
				GET_PARENT_SIG, null, null);
		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, this.className, PARENT_FIELD, 
				OBSERVABLE_TYPE);
		mv.visitInsn(ARETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();			
	}
	
	private void createMethod_setParent() {
		MethodVisitor mv;		
		mv = cv.visitMethod(ACC_PUBLIC, "setParent", 
				SET_PARENT_SIG, null, null);
		mv.visitCode();
		Label l0 = new Label();
		mv.visitLabel(l0);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitFieldInsn(PUTFIELD, this.className, PARENT_FIELD, 
				OBSERVABLE_TYPE);
		Label l1 = new Label();
		mv.visitLabel(l1);
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}
	
	private void createMethod_getPropertyChangeListeners() {
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PUBLIC, "getPropertyChangeListeners", 
				"()[Ljava/beans/PropertyChangeListener;", null, null);
		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitFieldInsn(GETFIELD, this.className, 
				PROPERTY_CHANGE_SUPPORT_FIELD, CPROPERTY_CHANGE_SUPPORT_TYPE);
		mv.visitMethodInsn(INVOKEVIRTUAL, CPROPERTY_CHANGE_SUPPORT,
				"getPropertyChangeListeners", 
				"()[Ljava/beans/PropertyChangeListener;");
		mv.visitInsn(ARETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}
	
	private void createMethod_fireAll() {
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_PRIVATE, "fireAll", "()V", null, null);
		mv.visitCode();		
		Map<String, PropertyConfig> configs = 
			Registry.getClassConfig(this.className).getPropertyConfigs();
		for (Map.Entry<String, PropertyConfig> entry : configs.entrySet()) {
			String property = entry.getKey();
			PropertyConfig config = entry.getValue();
			Type type = config.getType();
			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitFieldInsn(GETFIELD, this.className, 
					PROPERTY_CHANGE_SUPPORT_FIELD, 
					CPROPERTY_CHANGE_SUPPORT_TYPE);
			mv.visitLdcInsn(property);
			switch (type.getSort()) {
			case Type.OBJECT:
				mv.visitInsn(ACONST_NULL);
				break;
			case Type.BOOLEAN:
				mv.visitInsn(ICONST_M1);
				break;
			case Type.INT:
				mv.visitInsn(ICONST_M1);
				break;
			case Type.BYTE:
				mv.visitInsn(ICONST_M1);
				break;
			case Type.CHAR:
				mv.visitInsn(ICONST_M1);
				break;
			case Type.SHORT:
				mv.visitInsn(ICONST_M1);
				break;
			case Type.LONG:
				mv.visitInsn(LCONST_0);
				break;
			case Type.DOUBLE:
				mv.visitInsn(DCONST_0);
				break;
			case Type.FLOAT:
				mv.visitInsn(FCONST_0);
				break;
			}
			mv.visitVarInsn(ALOAD, 0);
			mv.visitFieldInsn(GETFIELD, this.className, property, 
					config.getTypeDescriptor());
			mv.visitMethodInsn(INVOKEVIRTUAL, 
					CPROPERTY_CHANGE_SUPPORT, 
					"firePropertyChange", 
					Strings.calcFMCSig(config.getType().getSort()));		
		}
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	
	@Override
	public MethodVisitor visitMethod(int access, String name, String desc,
			String signature, String[] exceptions) {
		
		MethodVisitor mv = 
			super.visitMethod(access, name, desc, signature, exceptions);

		if ("<init>".equals(name)) {
			mv = new BoundInitVisitor(mv, this.className);
		}
		
		// for non-static public observed set method
		if ((access & ACC_STATIC) == 0 && 
				name.startsWith("set") &&
				Type.getArgumentTypes(desc).length == 1) {
			String property = Strings.accessorToProperty(name);
			String typeDescriptor = 
				Type.getArgumentTypes(desc)[0].getDescriptor(); 
			
			mv = new SetMethodVisitor(mv, this.className, property, 
					typeDescriptor); 

			log.trace("[classname:" + this.className + 
					", name: " + name + 
					", desc: " + desc + 
					", signature: " + signature + 
					", exceptions: " + exceptions + "]");
			
			if (CTable.isCollectionType(typeDescriptor)) {
				// if this method is 'bound' and it's a collection...
				mv = new SetCollectionMethodVisitor(mv, 
						property, typeDescriptor);
			} else if (Registry.shouldEnhance(Strings.descriptorToClassname(typeDescriptor))) {
				// add code to check for Observable and nest
				mv = new SetObservableMethodVisitor(mv, this.className,
						property, typeDescriptor);
			}
		}
		return mv;
	}
}
